iT邦幫忙

2022 iThome 鐵人賽

DAY 8
0

昨天最後的「下篇預告」,筆者突然冒出一句:Python是可以在外部修改私有屬性的。這裡的「修改」,昨天沒有交代得很清楚,其實筆者想說的是:Python是可以使用「物件.屬性」的方式,來直接修改物件的私有屬性。


  • 噢,此語一出,簡直顛覆了我們的一貫邏輯!筆者不是一直在強調,私有屬性是不可以利用「物件.屬性」的方式更改的嗎?怎麼忽然又自打嘴巴了?
  • 在詳細解釋之前,讓我們稍為修改精簡一下昨天的程式碼,將焦點集中在今天主題:
    class Cult():  # 邪教
        def __init__(self, name: str):
            self.__name = name              #  private attribute
    
        def get_name(self) -> str:          # public method
            return self.__name
    
        def change_name(self, name: str):   # public method
            self.__name = name
    
    
    cult = Cult('奧修教')
    print(f'\n建立物件後的教名初值:{cult.get_name()}') # 1) 注意這裡是呼叫get_name()而非以cult.__name取值。
    
    cult.__name = '天堂之門'                            # 2) 直接賦值,沒有報錯耶。
    print(f'\n以cult.__name表示式賦值:{cult.__name=} (看來修改成功)')   # 3) 修改過後竟然就可以用cult.__name取值。 
    
    setattr(cult, '__name', '大衛教派')                 # 4) 改用setattr()函數測試。
    print(f'利用 setattr() 函數賦值:{cult.__name=} (看來修改成功)')     # 5) 看來也更改成功。
    print(f'         __name真正的值:{cult.get_name()} (其實沒有改到)')  # 6) 其實沒用,仍然是"奧修教"。
    
    print('\n呼叫class提供的change_name()方法修改:')
    cult.change_name('太陽聖殿教')
    print(f'         __name真正的值:{cult.get_name()} (這次才真的更改成功)')   # 7) 這次才真的更改成功。
    
    輸出如下:
    https://ithelp.ithome.com.tw/upload/images/20220923/201484854MA9qU6T0P.png
  • 程式說明:
    • 教名__name的初值是"奧修教"。
    • 物件.屬性的方式賦予新值,看來修改成功。為甚麼會成功?迷一。
    • 再試用Python的內設函數setattr()來賦值,看來也成功。
    • 注意:以上說的「成功」,是用物件.屬性來印出__name的值。迷二:明明之前說了n遍:「不能用物件.屬性」來取得私有屬性的值,何以在賦值成功之後,變成可以了?
    • 但是用類別提供的取值方法get_name(),得到的__name值卻依然是"奧修教",並沒有更改。各位要知道:用類別提供的方法來取值,取到的才是真正的值。
  • 到目前為止,「私有屬性不能在外部直接修改」的原則似乎並未動搖。
  • 問題是:既然直接用物件.屬性或setattr()的方式修改確定無效,又為甚麼沒有報錯,而且賦值後反而可以用物件.屬性印出__name的值呢?
  • 迷團一大堆。以下是解惑時間:

Python的private attribute內部實作

  • Python class的private,原來也不是百分百的private。
  • 知道門路的人,依然可以透過物件.屬性或物件.方法的表示式,存取私有屬性和私有方法。
  • Python內部對前綴但無後綴兩條底線的私有屬性/方法的內部實作,是在原來的私有名稱的前面,自動加上class名稱,再在class名稱前加一條底線(如果class名稱本身就有前綴底線則不加),而構成一個新的屬性/方法名稱。
  • 上例class是Cult,有一個private屬性__name,Python內部會將此屬性的名稱轉為_Cult__name
  • 所以如果真要用物件.屬性的表示式存取,要用的是改過後的名稱,而不是原名。
  • 上面的codecult.__name = '天堂之門'做的事,其實不是修改當初的私有屬性__name,而是創造了另外一個屬性。這個新屬性和原來的私有屬性__name無關。不信可以用print(cult.__dict__)來觀看cult的所有屬性,看過就真相大白了:
    https://ithelp.ithome.com.tw/upload/images/20220923/20148485j6rk9mq21b.png
  • 下面的code展示根據以上的「後門」,從外部真正修改私有屬性。至於私有方法的呼叫,原理一致,不贅舉:
    class Cult():  # 邪教
        def __init__(self, name: str):
            self.__name = name              #  private attribute
    
        def get_name(self) -> str:          # public method
            return self.__name
    
        def change_name(self, name: str):   # public method
            self.__name = name
    
    
    cult = Cult('奧修教')
    print(f'\n建立物件後的教名初值:{cult.get_name()}') # 1) 注意這裡是呼叫get_name()而非以cult.__name取值。
    
    cult._Cult__name = '天堂之門'                            # 2) 直接賦值,沒有報錯耶。
    print(f'\n以_Cult__name表示式賦值:{cult._Cult__name=}')   # 3) 修改過後竟然就可以用cult.__name取值。 
    print(f'         __name真正的值:{cult.get_name()} (真的修改成功)')  # 6) 其實沒用,仍然是"奧修教"。
    
    setattr(cult, '_Cult__name', '大衛教派')                 # 4) 改用setattr()函數測試。
    print(f'\n利用 setattr() 函數賦值:{cult._Cult__name=}')     # 5) 看來也更改成功。
    print(f'         __name真正的值:{cult.get_name()} (真的修改成功)')  # 6) 其實沒用,仍然是"奧修教"。
    
    # print('\n呼叫class提供的change_name()方法修改:')
    # cult.change_name('太陽聖殿教')
    # print(f'         __name真正的值:{cult.get_name()} (這次才真的更改成功)')   # 7) 這次才真的更改成功。
    
    輸出:
    https://ithelp.ithome.com.tw/upload/images/20220923/20148485WPRloHVM56.png
  • 至於為甚麼Python採用這種設計?有人說,是Guido van Rossum認為程式設計師都是「大人」,大人要為自己做的事負責。語言留一道「後門」,是給程式設計師方便,讓Pythonistas善加運用,而不是亂搞一通。
  • 如果以上說法屬實(筆者暫未查證),個人對仁慈獨裁者的這種設計哲學,其實是不贊同的。這種做法使得Python的類別沒有真正意義的私有屬性/方法,與其說讓我們程式設計師自行負責,不如說這是個有可能遭受攻擊的漏洞。就這點而言,筆者比較認同C-like語言的設計。

上一篇
Python也有Magic Johnson?
下一篇
Class為甚麼需要Private Attributes?
系列文
Oops! OOPP: An Introduction to Object-Oriented Programming in Python30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言